#!/usr/bin/env bash
set -e

count_only_flag=''
filter=''
nfilter=''
num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS:-1}
parallel_binary_name=${BATS_PARALLEL_BINARY_NAME:-"parallel"}
BATS_ABORT_ON_FAILURE=
bats_no_parallelize_across_files=${BATS_NO_PARALLELIZE_ACROSS_FILES-}
bats_no_parallelize_within_files=
filter_status=''
gather_tests_flags=('--dummy-flag')
flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
setup_suite_file=''
BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}"
BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=

abort() {
  printf 'Error: %s\n' "$1" >&2
  exit 1
}

# shellcheck source=lib/bats-core/common.bash disable=SC2153
source "$BATS_ROOT/$BATS_LIBDIR/bats-core/common.bash"

while [[ "$#" -ne 0 ]]; do
  case "$1" in
  -c)
    count_only_flag=1
    ;;
  -f)
    shift
    filter="$1"
    ;;
  -j)
    shift
    num_jobs="$1"
    flags+=('-j' "$num_jobs")
    ;;
  --parallel-binary-name)
    shift
    parallel_binary_name="$1"
    ;;
  -T)
    flags+=('-T')
    ;;
  -x)
    flags+=('-x')
    ;;
  --abort)
    flags+=('--abort')
    BATS_ABORT_ON_FAILURE=1
    ;;
  --no-parallelize-across-files)
    bats_no_parallelize_across_files=1
    ;;
  --no-parallelize-within-files)
    bats_no_parallelize_within_files=1
    flags+=("--no-parallelize-within-files")
    ;;
  --filter-status)
    shift
    filter_status="$1"
    ;;
  --filter-tags)
    shift
    gather_tests_flags+=("--filter-tags" "$1")
    ;;
  --negative-filter)
    shift
    nfilter="$1"
    ;;
  --dummy-flag) ;;

  --trace)
    flags+=('--trace')
    ((++BATS_TRACE_LEVEL)) # avoid returning 0
    ;;
  --print-output-on-failure)
    flags+=(--print-output-on-failure)
    ;;
  --show-output-of-passing-tests)
    flags+=(--show-output-of-passing-tests)
    BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=1
    ;;
  --verbose-run)
    flags+=(--verbose-run)
    ;;
  --gather-test-outputs-in)
    shift
    flags+=(--gather-test-outputs-in "$1")
    ;;
  --setup-suite-file)
    shift
    setup_suite_file="$1"
    ;;
  *)
    break
    ;;
  esac
  shift
done

if [[ "$num_jobs" != 1 ]]; then
  if ! type -p "${parallel_binary_name}" >/dev/null && "${parallel_binary_name}" --version &>/dev/null && [[ -z "$bats_no_parallelize_across_files" ]]; then
    abort "Cannot execute \"${num_jobs}\" jobs without GNU parallel"
  fi
  # shellcheck source=lib/bats-core/semaphore.bash
  source "${BATS_ROOT}/$BATS_LIBDIR/bats-core/semaphore.bash"
  bats_semaphore_setup
fi

# create a file that contains all (filtered) tests to run from all files
TESTS_LIST_FILE="${BATS_RUN_TMPDIR}/test_list_file.txt"

TEST_ROOT=${1-}
TEST_ROOT=${TEST_ROOT%/*}
export BATS_RUN_LOGS_DIRECTORY="$TEST_ROOT/.bats/run-logs"
if [[ ! -d "$BATS_RUN_LOGS_DIRECTORY" ]]; then
  if [[ -n "$filter_status" ]]; then
    printf "Error: --filter-status needs '%s/' to save failed tests. Please create this folder, add it to .gitignore and try again.\n" "$BATS_RUN_LOGS_DIRECTORY"
    exit 1
  else
    BATS_RUN_LOGS_DIRECTORY=
  fi
  # discard via sink instead of having a conditional later
  export BATS_RUNLOG_FILE='/dev/null'
else
  # use UTC (-u) to avoid problems with TZ changes
  BATS_RUNLOG_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
  export BATS_RUNLOG_FILE="$BATS_RUN_LOGS_DIRECTORY/${BATS_RUNLOG_DATE}.log" BATS_RUNLOG_DATE
  if [[ ! -w "$BATS_RUN_LOGS_DIRECTORY" ]]; then
    printf "WARNING: Cannot write in %s. This run will not write a log!\n" "$BATS_RUN_LOGS_DIRECTORY" >&2
    BATS_RUNLOG_FILE='/dev/null' # disable runlog file
  fi
fi

# create file even if no tests are found so the `read` below won't fail
touch "$TESTS_LIST_FILE"
gather_tests_output=$(TESTS_LIST_FILE="$TESTS_LIST_FILE" filter="$filter" nfilter="$nfilter" filter_status="$filter_status" bats-gather-tests "${gather_tests_flags[@]}" -- "$@")
test_count=$(grep -c ^ "$TESTS_LIST_FILE" || true) # don't fail here when there are no tests
if [[ $gather_tests_output == focus_mode ]]; then
  focus_mode=1
else
  focus_mode=
fi

if [[ -n "$count_only_flag" ]]; then
  printf '%d\n' "${test_count}"
  # we don't want to pollute the runlog files, as no tests would have been run
  [[ $BATS_RUNLOG_FILE != /dev/null ]] && rm "$BATS_RUNLOG_FILE"
  exit 0
fi

if [[ -n "$bats_no_parallelize_across_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
  abort "The flag --no-parallelize-across-files requires at least --jobs 2"
fi

if [[ -n "$bats_no_parallelize_within_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
  abort "The flag --no-parallelize-across-files requires at least --jobs 2"
fi

# only abort on the lowest levels
trap 'BATS_INTERRUPTED=true' INT

if [[ -n "$focus_mode" ]]; then
  printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n" >&2
fi

bats_exit_suite() {
if [[ "$focus_mode" == 1 && $bats_exec_suite_status -eq 0 ]]; then
  if [[ ${BATS_NO_FAIL_FOCUS_RUN-} == 1 ]]; then
    printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n" >&2
  else
    printf "Marking test run as failed due to \`bats:focus\` tag. (Set \`BATS_NO_FAIL_FOCUS_RUN=1\` to disable.)\n" >&2
    bats_exec_suite_status=1
  fi
fi

exit "$bats_exec_suite_status" # the actual exit code will be set by the exit trap using bats_exec_suite_status
}

bats_exec_suite_status=0
printf '1..%d\n' "${test_count}"

# No point on continuing if there's no tests.
if [[ "${test_count}" == 0 ]]; then
  bats_exec_suite_status=0
  bats_exit_suite
fi

export BATS_SUITE_TMPDIR="${BATS_RUN_TMPDIR}/suite"
if ! mkdir "$BATS_SUITE_TMPDIR"; then
  printf '%s\n' "Failed to create BATS_SUITE_TMPDIR" >&2
  exit 1
fi

# Deduplicate filenames (without reordering) to avoid running duplicate tests n by n times.
# (see https://github.com/bats-core/bats-core/issues/329)
# If a file was specified multiple times, we already got it repeatedly in our TESTS_LIST_FILE.
# Thus, it suffices to bats-exec-file it once to run all repeated tests on it.
IFS=$'\n' read -d '' -r -a BATS_UNIQUE_TEST_FILENAMES < <(printf "%s\n" "$@" | nl | sort -k 2 | uniq -f 1 | sort -n | cut -f 2-) || true

# shellcheck source=lib/bats-core/tracing.bash
source "$BATS_ROOT/$BATS_LIBDIR/bats-core/tracing.bash"
bats_setup_tracing

trap bats_suite_exit_trap EXIT

exec 3<&1

# shellcheck disable=SC2317,SC2329
bats_suite_exit_trap() {
  local print_bats_out="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS}"
  if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" || -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then
    if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" ]]; then
      printf "not ok 1 setup_suite\n"
    elif [[ -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then
      printf "not ok %d teardown_suite\n" $((test_count + 1))
    fi
    local stack_trace
    bats_get_failure_stack_trace stack_trace
    bats_print_stack_trace "${stack_trace[@]}"
    bats_print_failed_command "${stack_trace[@]}"
    print_bats_out=1
    bats_exec_suite_status=1
  fi

  if [[ -n "$print_bats_out" ]]; then
    bats_prefix_lines_for_tap_output <"$BATS_OUT"
  fi

  if [[ ${BATS_INTERRUPTED-NOTSET} != NOTSET ]]; then
    printf "\n# Received SIGINT, aborting ...\n\n"
  fi

  if [[ -d "$BATS_RUN_LOGS_DIRECTORY" && -n "${BATS_INTERRUPTED:-}" ]]; then
    # aborting a test run with CTRL+C does not save the runlog file
    [[ "$BATS_RUNLOG_FILE" != /dev/null ]] && rm "$BATS_RUNLOG_FILE"
  fi
  exit "$bats_exec_suite_status"
} >&3

bats_run_teardown_suite() {
  local bats_teardown_suite_status=0
  # avoid being called twice, in case this is not called through bats_teardown_suite_trap
  # but from the end of file
  trap bats_suite_exit_trap EXIT
  
  bats_set_stacktrace_limit

  BATS_TEARDOWN_SUITE_COMPLETED=
  teardown_suite >>"$BATS_OUT" 2>&1 || bats_teardown_suite_status=$?
  if ((bats_teardown_suite_status == 0)); then
    BATS_TEARDOWN_SUITE_COMPLETED=1
  elif [[ -n "${BATS_SETUP_SUITE_COMPLETED:-}" ]]; then
    BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
    BATS_ERROR_STATUS=$bats_teardown_suite_status
    return $BATS_ERROR_STATUS
  fi
}

# shellcheck disable=SC2317,SC2329
bats_teardown_suite_trap() {
  bats_run_teardown_suite
  bats_suite_exit_trap
}

teardown_suite() {
  :
}

trap bats_teardown_suite_trap EXIT

BATS_OUT="$BATS_RUN_TMPDIR/suite.out"
if [[ -n "$setup_suite_file" ]]; then
  setup_suite() {
    printf "%s does not define \`setup_suite()\`\n" "$setup_suite_file" >&2
    return 1
  }

  # shellcheck disable=SC2034 # will be used in the sourced file below
  BATS_TEST_FILENAME="$setup_suite_file"
  # shellcheck source=lib/bats-core/test_functions.bash
  source "$BATS_ROOT/$BATS_LIBDIR/bats-core/test_functions.bash"
  _bats_test_functions_setup -1 # invalid TEST_NUMBER, as this is not a test

  # shellcheck disable=SC1090
  source "$setup_suite_file"

  bats_set_stacktrace_limit

  set -eET
  export BATS_SETUP_SUITE_COMPLETED=
  setup_suite >>"$BATS_OUT" 2>&1
  BATS_SETUP_SUITE_COMPLETED=1
  set +ET
else
  # prevent exit trap from printing an error because of incomplete setup_suite,
  # when there was none to execute
  BATS_SETUP_SUITE_COMPLETED=1
fi

if [[ "$num_jobs" -gt 1 ]] && [[ -z "$bats_no_parallelize_across_files" ]]; then
  parallel_flags=(--keep-order --jobs "$num_jobs")
  if (( BATS_ABORT_ON_FAILURE )); then
    parallel_flags+=('--halt' 'now,fail=1')
  fi
  # run files in parallel to get the maximum pool of parallel tasks
  # shellcheck disable=SC2086,SC2068
  # we need to handle the quoting of ${flags[@]} ourselves,
  # because parallel can only quote it as one
  "${parallel_binary_name}" "${parallel_flags[@]}" -- "bats-exec-file" "$(printf "%q " "${flags[@]}")" "{#}" "{}" "$TESTS_LIST_FILE" <<< "$(printf "%s\n" "${BATS_UNIQUE_TEST_FILENAMES[@]}")" 2>&1 || bats_exec_suite_status=1
else
  file_index=0
  for filename in "${BATS_UNIQUE_TEST_FILENAMES[@]}"; do
    (( ++file_index ))
    if [[ "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then
      bats_exec_suite_status=130 # bash's code for SIGINT exits
      break
    fi
    bats-exec-file "${flags[@]}" "$file_index" "$filename" "${TESTS_LIST_FILE}" || bats_exec_suite_status=1
    if (( BATS_ABORT_ON_FAILURE && bats_exec_suite_status > 0 )); then
      break
    fi
  done
fi

set -eET
bats_run_teardown_suite

bats_exit_suite
